package com.guli.mall.pms.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.guli.common.pojo.vo.TreeNode;
import com.guli.common.utils.ListToTree;
import com.guli.mall.pms.ProductCategoryApp;
import com.guli.mall.pms.feign.ProductServiceFeignClient;
import com.guli.mall.pms.util.ListToTree2;
import com.guli.mall.serviceBase.globalException.MallException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.BooleanUtils;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.guli.common.utils.PageUtils;
import com.guli.common.utils.Query;

import com.guli.mall.pms.mapper.CategoryMapper;
import com.guli.mall.pms.entity.CategoryEntity;
import com.guli.mall.pms.service.CategoryService;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;

@Slf4j
@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, CategoryEntity> implements CategoryService {

    @Resource
    ProductServiceFeignClient productServiceFeignClient;

    @Resource
    StringRedisTemplate stringRedisTemplate;

    Lock lock = new ReentrantLock();

    @Override
    public PageUtils queryPage(Integer currentPage, Integer size) {

        Page<CategoryEntity> page = new Page<>(currentPage, size);

        Page<CategoryEntity> categoryEntityPage = this.page(page);

        return new PageUtils(categoryEntityPage);
    }

    //@Cacheable(cacheNames = CATEGORY,key = "'threeNodeListPrimary'")
    @Override
    public TreeNode<CategoryEntity> treeNodeList() {

        ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();

        TreeNode<CategoryEntity> result = getTreeNodeListFromCache(ops);

        if (!ObjectUtils.isEmpty(result)) {
            return result;
        }

        return writeToRedisWithDistributedLock(ops);
    }


    private static final String TREE_NODE_LIST_KEY = "treeNodeListKey";

    private static final String TREE_NODE_LIST_LOCK = "treeNodeListLock";

    /**
     * 使用 redis 的分布式锁 来使得分布式应用只有一个去查询数据库，并将查询到的结果放入 redis
     *
     * @param ops
     * @return
     */
    private TreeNode<CategoryEntity> writeToRedisWithDistributedLock(ValueOperations<String, String> ops) {

        String uuid = UUID.randomUUID().toString();
        /**
         * 1、占领分布式锁
         * 不存在该 key 的时候才能设置成功
         * 通过第三步可以得知，占锁和设置过期时间，必须是一个原子操作（同步的），要放在一起
         * */
        Boolean treeNodeListLock = ops.setIfAbsent(TREE_NODE_LIST_LOCK, uuid, 10, TimeUnit.SECONDS);

        TreeNode<CategoryEntity> result = null;

        if (!ObjectUtils.isEmpty(treeNodeListLock) && treeNodeListLock) {

            log.info("{}持有分布式锁",Thread.currentThread().getName());
            /**
             * 2、这个线程占用到redis了（加锁成功）
             * */
            try {

                //stringRedisTemplate.expire("treeNodeListLock",10,TimeUnit.SECONDS); // 3、设置过期时间，这样还是一样的问题，如果在执行这行代码之前，断电了，就死锁了

                result = queryCategoryAndSetIntoRedis(ops);

            } finally {
                /**
                 * 防止死锁
                 * 问题：但是如果，机器断电，锁还是删不掉
                 * 添加判断锁的内容是不是自己放进去的，如果是自己的锁，就能放心删除
                 * */
                //if(uuid.equals(ops.get("treeNodeListLock"))){
                /**
                 *  删除我自己的锁，但是这两个操作不是原子操作，虽然确认到是我自己的锁，
                 *  但是在发送删除操作之前，锁被更新成其他人的锁了，这样删掉的还是其他人的锁
                 *  官方的解决方式 Lua 脚本解锁
                 */
                //   stringRedisTemplate.delete("treeNodeListLock");
                //}
                String luaScript = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";

                /**
                 * 127.0.0.1:6379> del aa
                 * (integer) 1
                 * 127.0.0.1:6379> del aa
                 * (integer) 0
                 * redis 删除成功失败，都会返回 integer
                 */
                RedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class); // integer 为 脚本返回的类型

                /**
                 * 使用 lua 脚本使得，查询和删除是一个原子操作
                 *  lua 脚本中的 KEYS[1] 就是 Collections.singletonList("treeNodeListLock").get(0)
                 *  lua 脚本中的 ARGV[1] 就是 uuid
                 *
                 *  如果使用 lettuce-core 执行脚本的时候，还会报异常，所以还是切回 jedis
                 * */
                stringRedisTemplate.execute(script, Collections.singletonList(TREE_NODE_LIST_LOCK), uuid);
            }
            return result;
        } else {
            /**
             * 加锁失败，要重试
             * */
/*            do { // 这样，如果拿到锁的线程断电了，如果没有新线程来获取锁，这样所有的 while 中，将没有线程去顶替，会直到栈溢出
                result = getTreeNodeListFromCache(ops);

                System.out.println(Thread.currentThread().getName() + "自旋等待结果");

                try {
                    TimeUnit.MILLISECONDS.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            } while (ObjectUtils.isEmpty(result));*/

            log.info("{}自旋",Thread.currentThread().getName());

            // 不睡觉，就是想栈溢出
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            /**
             * 加锁失败，自旋，注意自旋调用的不是本方法，而是在本方法的基础上还要查一遍 redis 查看是否有数据
             * 所以就调用父方法，它刚好执行了这个步骤，
             * 而不是上来就去抢占锁
             * */
            return treeNodeList();
        }
    }


    /**
     * 通过本地锁的方式将从 mysql中查询到的数据放入redis中，并返回查询到的结果
     *
     * @param ops
     * @return
     */
    private TreeNode<CategoryEntity> writeToRedisWithLocalLock(ValueOperations<String, String> ops) {
        lock.lock();

        try {
            /**
             * 得到锁以后，我们应该再去缓存中确定一次，说不定上一个任务已经查好了并放在了缓存里
             * */
            TreeNode<CategoryEntity> result = getTreeNodeListFromCache(ops);

            if (!ObjectUtils.isEmpty(result)) {
                return result;
            }

            return queryCategoryAndSetIntoRedis(ops);

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

        return null;
    }

    /**
     * 从 redis 中获取 treeNodeList 对应的 value
     *
     * @param ops
     * @return 如果存在，就返回，如果不存在，返回 null
     */
    private TreeNode<CategoryEntity> getTreeNodeListFromCache(ValueOperations<String, String> ops) {

        String treeNodeList = ops.get(TREE_NODE_LIST_KEY);

        if (!StringUtils.isEmpty(treeNodeList)) {
            TypeReference<TreeNode<CategoryEntity>> type = new TypeReference<TreeNode<CategoryEntity>>() {
            };
            return JSON.parseObject(treeNodeList, type);
        }

        return null;
    }

    /**
     * 去 mysql 中查询category 整理成 treeNodeList 之后，再放入 redis
     *
     * @param ops
     * @return
     */
    private TreeNode<CategoryEntity> queryCategoryAndSetIntoRedis(ValueOperations<String, String> ops) {
        List<CategoryEntity> list = this.list();

        //方式一：因为ElementUI不可以解析对象中的属性 node.name 作为标签的名字，所以要使用方式二
        TreeNode<CategoryEntity> treeNode = new TreeNode<>();

        Comparator<TreeNode<CategoryEntity>> comparator = (TreeNode<CategoryEntity> treeNode1, TreeNode<CategoryEntity> treeNode2) -> {
            return treeNode1.getNode().getSort() - treeNode2.getNode().getSort();
        };

        ListToTree.parse(list, treeNode, 0L, comparator);

        ops.set(TREE_NODE_LIST_KEY, JSON.toJSONString(treeNode));

        return treeNode;
    }

    private static final String TREE_CATEGORY_LIST = "treeCategoryList";

    private static final String TREE_CATEGORY_LIST_CACHE_KEY = "'treeCategoryList'";

    // 缓存名字
    private static final String CATEGORY = "product-category";


    @Cacheable(cacheNames = CATEGORY, key = TREE_CATEGORY_LIST_CACHE_KEY,sync = true)
    public CategoryEntity treeCategoryList() {
        List<CategoryEntity> list = this.list();


        Comparator<CategoryEntity> comparator = (categoryEntity1, categoryEntity2) -> {
            return (categoryEntity1.getSort() == null ? 0 : categoryEntity1.getSort()) - (categoryEntity2.getSort() == null ? 0 : categoryEntity2.getSort());
        };

        CategoryEntity entity = new CategoryEntity();

        entity.setName("根节点");

        ListToTree2.parse(list, entity, 0L, comparator);

        return entity;
    }

    /**
     * 1、同时进行多种缓存操作
     * 2、指定删除某个分区下的所有数据，@CacheEvict(cacheNames = CATEGORY,allEntries = true)
     * 3、一个缓存分区存储同一类型的数据，分区名就当作缓存的前缀，方便删除缓存分区
     * @param category
     */
    @Caching(evict = {
            @CacheEvict(cacheNames = CATEGORY,allEntries = true)
    })
    @Transactional
    @Override
    public void updateBatch(List<CategoryEntity> category) {
        this.updateBatchById(category);
    }

    @Override
    public List<Long> findCategoryIdPath(Long categoryId) {

        List<Long> longs = new ArrayList<>();

        longs.add(categoryId);

        CategoryEntity categoryEntity = null;

        int count = 0;

        do {
            categoryEntity = this.getById(categoryId);

            if (ObjectUtils.isEmpty(categoryEntity)) {
                throw new MallException(20001, categoryId + "对应的目录不存在");
            }

            longs.add(categoryEntity.getParentCid());

            categoryId = categoryEntity.getParentId();

            count++;

        } while (!(categoryEntity.getParentCid() == 0) && count < 4);
        return longs;
    }

    @Caching(evict = {
            @CacheEvict(cacheNames = CATEGORY,allEntries = true)
    })
    //TODO 需要改为分布式事务
    @Transactional
    @Override
    public void updateBatchDetail(List<CategoryEntity> asList) {
        this.updateBatch(asList);

        for (CategoryEntity categoryEntity : asList) {
            /**
             * 更新 category-brand-relation 中的categoryName冗余数据
             * */
            if (!StringUtils.isEmpty(categoryEntity.getName())) {

                productServiceFeignClient.updateCategoryName(categoryEntity.getId(), categoryEntity.getName());
            }
        }

    }

    @Caching(evict = {
            @CacheEvict(cacheNames = CATEGORY,allEntries = true)
    })
    @CacheEvict(cacheNames = TREE_CATEGORY_LIST, key = "'key'")
    @Transactional
    @Override
    public void removeByCategoryIds(List<Long> cateGoryIds) {
        /**
         * 1.先查询该目录是否被其他目录所引用
         * */

        cateGoryIds.stream().forEach(categoryId -> {

                    QueryWrapper<CategoryEntity> parent_cid = new QueryWrapper<CategoryEntity>().eq("parent_cid", categoryId);

                    int count = this.count(parent_cid);

                    if (count > 0) {
                        throw new MallException(30001, "商品目录id = " + categoryId + "的还含有" + count + "个下级目录，不能删除");
                    }

                }
        );

        this.removeByIds(cateGoryIds);
    }

}